package dev.ece.util;

import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import org.lwjgl.opengl.GL11;

import dev.ece.util.lib.Matrix4f;
import dev.ece.util.lib.Vector3f;
import dev.ece.util.lib.Vector4f;
import dev.ece.util.tools.Screen;
import dev.ece.util.tools.transformations.Transformation;

/**
 * 工具类
 * @author 潜行的青衣
 * @see
 */
public class GLUtils {
	
	private static Matrix4f ProjectionMatrix = new Matrix4f();
	
	private static final Stack<Matrix4f> ProjectionMatrixStack = new Stack<Matrix4f>();
	
	private static Matrix4f ModelViewMatrix = new Matrix4f();
	
	private static final Stack<Matrix4f> ModelViewMatrixStack = new Stack<Matrix4f>();
	
	private static final Matrix4f NormalMatrix = new Matrix4f();
	
	private static final Matrix4f ViewMatrix = new Matrix4f();
	
	private static final Matrix4f ProjectionViewMatrix = new Matrix4f();
	
	private static final Matrix4f ModelViewProjectionMatrix = new Matrix4f();
	
	/**
	 * 设置投影
	 * @param width
	 * @param height
	 */
	public static void glProjection3D(Screen screen) {
		float width = screen.getWidth(); 
		float height = screen.getHeight();
		float ratio;
		ProjectionMatrix.identity();
		//3D透视屏
		if(width > height) {
			ratio = width / height;
			ProjectionMatrix.frustum(
					-ratio, ratio,
					-1, 1,
					1f, 
					1024f);
		} else {
			ratio = height / width;
			ProjectionMatrix.frustum(
					-1, 1, 
					-ratio, ratio, 
					1f, 
					1024f);
		}
		ModelViewMatrix.identity();
	}
	
	public static void frustum(
			float left, float right, 
			float bottom, float top, 
			float near, float far) {
		ProjectionMatrix.identity();
		ProjectionMatrix.frustum(
				left, right, 
				bottom, top, 
				near, far);
		ModelViewMatrix.identity();
	}
	
	/**
	 * 设置投影
	 * @param width
	 * @param height
	 */
	public static void glProjection2D(Screen screen) {
		float width = screen.getWidth(); 
		float height = screen.getHeight();
		//重置投影矩阵
		float h_width = width / 2;
		float h_height = height / 2;
		ProjectionMatrix.identity();
		//计算投影矩阵
		ProjectionMatrix.ortho(
				-h_width, h_width, 
				-h_height, h_height, 
				1f, 
				24f);
		ModelViewMatrix.identity();
	}
	
	/**
	 * 设置投影
	 * @param width
	 * @param height
	 */
	public static void ortho(
			float left, float right, 
			float bottom, float top, 
			float near, float far) {
		ProjectionMatrix.identity();
		//计算投影矩阵
		ProjectionMatrix.ortho(
				left, right, 
				bottom, top, 
				near, 
				far);
		ModelViewMatrix.identity();
	}
	
	/**
	 * 设置观察位置
	 * @param eyeX
	 * @param eyeY
	 * @param eyeZ
	 * @param centerX
	 * @param centerY
	 * @param centerZ
	 * @param upX
	 * @param upY
	 * @param upZ
	 */
	public static void lookAt(Vector3f eye, Vector3f center,Vector3f head) {
		ViewMatrix.identity();
		ViewMatrix.lookAt(eye, center, head);
		ProjectionViewMatrix.mul(ProjectionMatrix, ViewMatrix);
	}
	
	/**
	 * 开启透明效果
	 */
	public static void glEnableAlpha() {
		GL11.glEnable(GL11.GL_BLEND);
		// 基于源象素alpha通道值的半透明混合函数
		GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
	}
	
	/**
	 * 关闭透明效果
	 */
	public static void glDisableAlpha() {
		GL11.glDisable(GL11.GL_BLEND);
	}
	
	/**
	 * 启动深度测试
	 */
	public static void glEnableDepthTest() {
		GL11.glEnable(GL11.GL_DEPTH_TEST);
	}
	
	/**
	 * 关闭深度测试
	 */
	public static void glDisableDepthTest() {
		GL11.glDisable(GL11.GL_DEPTH_TEST);
	}
	
	/**
	 * 投影矩阵压栈
	 */
	public static void glPushProjectionMatrix() {
		ProjectionMatrixStack.push(ProjectionMatrix.clone());
	}
	
	/**
	 * 投影矩阵出栈
	 */
	public static void glPopProjectionMatrix() {
		ProjectionMatrix = ProjectionMatrixStack.pop();
	}

	/**
	 * 模型矩阵压栈
	 */
	public static void glPushMatrix() {
		ModelViewMatrixStack.push(ModelViewMatrix.clone());
	}
	
	/**
	 * 模型矩阵出栈
	 */
	public static void glPopMatrix() {
		ModelViewMatrix = ModelViewMatrixStack.pop();
	}
	
	/**
	 * 缩放功能
	 * @param scale
	 */
	public static void scale(Vector3f scale) {
		ModelViewMatrix.scale(scale);
	}
	
	/**
	 * 缩放功能
	 * @param x
	 * @param y
	 * @param z
	 */
	public static void scale(float x, float y, float z) {
		ModelViewMatrix.scale(x, y, z);
	}
	
	/**
	 * 位移矩阵
	 * @param position
	 */
	public static void translate(Vector3f position) {
		ModelViewMatrix.translate(position);
	}
	
	/**
	 * 位移矩阵
	 * @param x
	 * @param y
	 * @param z
	 */
	public static void translate(float x, float y, float z) {
		ModelViewMatrix.translate(x, y, z);
	}
	
	/**
	 * 旋转矩阵
	 * @param rotate
	 */
	public static void rotate(Vector4f rotate) {
		ModelViewMatrix.rotate(rotate);
	}
	
	/**
	 * 旋转矩阵
	 * @param x
	 * @param y
	 * @param z
	 * @param angle
	 */
	public static void rotate(float x, float y, float z, float angle) {
		ModelViewMatrix.rotate(x, y, z, angle);
	}
	
	/**
	 * 最接近number并且大于等于number的2的n次幂整数
	 * @param number
	 * @return
	 */
	public static int pow(double number) {
		if(number < 2)
			return 2;
		int i = 1;
		int result = 0;
		while(result < number) {
			result = (int) Math.pow(2, i ++);
		}
		return result;
	}
	/**
	 * 计算最终矩阵矩阵和法线矩阵
	 */
	public static void computeModelViewProjectionMatrix() {
		ModelViewProjectionMatrix.mul(ProjectionViewMatrix, ModelViewMatrix);
		NormalMatrix.invert(ModelViewMatrix);
		NormalMatrix.transpose();
	}
	
	/**
	 * 矩阵转换
	 * @param transforms
	 * @param matrix
	 */
	public static void transform(List<Transformation> transformations, Matrix4f matrix) {
		if(!transformations.isEmpty()) {
			Iterator<Transformation> it = transformations.iterator();
			while(it.hasNext()) {
				it.next().transform(matrix);
			}
		}
	}
	
	/**
	 * 矩阵转换
	 * @param transforms
	 */
	public static void transform(List<Transformation> transformations) {
		transform(transformations, getModelViewMatrix());
	}
	
	
	/**
     * Map object coordinates into window coordinates. gluProject transforms the
     * specified object coordinates into window coordinates using model, proj,
     * and view. The result is stored in win.
     * <p>
     * Note that you can use the OES_matrix_get extension, if present, to get
     * the current modelView and projection matrices.
     *
     * @param objX object coordinates X
     * @param objY object coordinates Y
     * @param objZ object coordinates Z
     * @param model the current modelview matrix
     * @param modelOffset the offset into the model array where the modelview
     *        maxtrix data starts.
     * @param project the current projection matrix
     * @param projectOffset the offset into the project array where the project
     *        matrix data starts.
     * @param view the current view, {x, y, width, height}
     * @param viewOffset the offset into the view array where the view vector
     *        data starts.
     * @param win the output vector {winX, winY, winZ}, that returns the
     *        computed window coordinates.
     * @param winOffset the offset into the win array where the win vector data
     *        starts.
     * @return A return value of GL_TRUE indicates success, a return value of
     *         GL_FALSE indicates failure.
     */
    public static int gluProject(float objX, float objY, float objZ,
            float[] model, int modelOffset, float[] project, int projectOffset,
            int[] view, int viewOffset, float[] win, int winOffset) {
        float[] scratch = sScratch;
        synchronized(scratch) {
            final int M_OFFSET = 0; // 0..15
            final int V_OFFSET = 16; // 16..19
            final int V2_OFFSET = 20; // 20..23
            Matrix.multiplyMM(scratch, M_OFFSET, project, projectOffset,
                    model, modelOffset);

            scratch[V_OFFSET + 0] = objX;
            scratch[V_OFFSET + 1] = objY;
            scratch[V_OFFSET + 2] = objZ;
            scratch[V_OFFSET + 3] = 1.0f;

            Matrix.multiplyMV(scratch, V2_OFFSET,
                    scratch, M_OFFSET, scratch, V_OFFSET);

            float w = scratch[V2_OFFSET + 3];
            if (w == 0.0f) {
                return GL11.GL_FALSE;
            }

            float rw = 1.0f / w;

            win[winOffset] =
                    view[viewOffset] + view[viewOffset + 2]
                            * (scratch[V2_OFFSET + 0] * rw + 1.0f)
                            * 0.5f;
            win[winOffset + 1] =
                    view[viewOffset + 1] + view[viewOffset + 3]
                            * (scratch[V2_OFFSET + 1] * rw + 1.0f) * 0.5f;
            win[winOffset + 2] = (scratch[V2_OFFSET + 2] * rw + 1.0f) * 0.5f;
        }

        return GL11.GL_TRUE;
    }

    /**
     * Map window coordinates to object coordinates. gluUnProject maps the
     * specified window coordinates into object coordinates using model, proj,
     * and view. The result is stored in obj.
     * <p>
     * Note that you can use the OES_matrix_get extension, if present, to get
     * the current modelView and projection matrices.
     *
     * @param winX window coordinates X
     * @param winY window coordinates Y
     * @param winZ window coordinates Z
     * @param model the current modelview matrix
     * @param modelOffset the offset into the model array where the modelview
     *        maxtrix data starts.
     * @param project the current projection matrix
     * @param projectOffset the offset into the project array where the project
     *        matrix data starts.
     * @param view the current view, {x, y, width, height}
     * @param viewOffset the offset into the view array where the view vector
     *        data starts.
     * @param obj the output vector {objX, objY, objZ}, that returns the
     *        computed object coordinates.
     * @param objOffset the offset into the obj array where the obj vector data
     *        starts.
     * @return A return value of GL10.GL_TRUE indicates success, a return value
     *         of GL10.GL_FALSE indicates failure.
     */
    public static int gluUnProject(float winX, float winY, float winZ,
            float[] model, int modelOffset, float[] project, int projectOffset,
            int[] view, int viewOffset, float[] obj, int objOffset) {
        float[] scratch = sScratch;
        synchronized(scratch) {
            final int PM_OFFSET = 0; // 0..15
            final int INVPM_OFFSET = 16; // 16..31
               final int V_OFFSET = 0; // 0..3 Reuses PM_OFFSET space
            Matrix.multiplyMM(scratch, PM_OFFSET, project, projectOffset,
                    model, modelOffset);

            if (!Matrix.invertM(scratch, INVPM_OFFSET, scratch, PM_OFFSET)) {
                return GL11.GL_FALSE;
            }

            scratch[V_OFFSET + 0] =
                    2.0f * (winX - view[viewOffset + 0]) / view[viewOffset + 2]
                            - 1.0f;
            scratch[V_OFFSET + 1] =
                    2.0f * (winY - view[viewOffset + 1]) / view[viewOffset + 3]
                            - 1.0f;
            scratch[V_OFFSET + 2] = 2.0f * winZ - 1.0f;
            scratch[V_OFFSET + 3] = 1.0f;

            Matrix.multiplyMV(obj, objOffset, scratch, INVPM_OFFSET,
                    scratch, V_OFFSET);
        }

        return GL11.GL_TRUE;
    }
    
    private static final float[] sScratch = new float[32];
    
	public static Matrix4f getProjectionMatrix() {
		return ProjectionMatrix;
	}

	public static Matrix4f getModelViewMatrix() {
		return ModelViewMatrix;
	}
	
	public static Matrix4f getViewMatrix() {
		return ViewMatrix;
	}
	
	public static Matrix4f getNormalMatrix() {
		return NormalMatrix;
	}

	public static Matrix4f getModelViewProjectionMatrix() {
		return ModelViewProjectionMatrix;
	}
	
	public static Matrix4f getProjectionViewMatrix() {
		return ProjectionViewMatrix;
	}
}
